Design Simple Chat Application

Ashish

Ashish Pratap Singh

medium

In this chapter, we will explore the low-level design of a simple in-memory chat application.

Let's start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.

Here is an example of how a conversation between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Support one-on-one and group messaging
  • Allow users to view their complete chat history
  • Ensure message ordering is preserved, i.e., messages must be delivered in the order they were sent

1.2 Non-Functional Requirements

  • Modularity: The system should follow object-oriented design principles with well-defined components.
  • Scalability: The system must support many concurrent users and deliver messages in real time with minimal latency.
  • Extensibility: The design should be flexible enough to support future features like file sharing, typing indicators, or message reactions
  • Maintainability: Code should be clean, testable, and easy to update or extend as requirements evolve.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., user, message, chat, session, contact list) and actions (e.g., authenticate, send, receive, display, store) from the functional requirements. These often translate directly into classes, enums, or interfaces in an object-oriented design.

Below, we break down the functional requirements and extract the relevant entities. Related requirements are grouped together when they represent the same conceptual unit.

1. The system should support both one-on-one and group chats.

This suggests the need for a Chat entity that can represent either a one-on-one chat or a group conversation. Each chat has a list of participating users and a collection of messages.

Additionally, we need a ChatService to act as the orchestrator. It will be responsible for creating new chats, adding participants, retrieving chat history, and managing chat-level operations.

2. Users should be able to send and receive messages.

This introduces the User entity, which represents each participant in the system. A user can be part of multiple chats and can send or receive messages.

Messages themselves are modeled using the Message entity, which includes sender, timestamp and the message content.

These core entities define the essential abstractions of a simple chat application and will guide the structure of your low-level design and class diagrams.

3. Designing Classes and Relationships

This section outlines the classes that form the core of the chat application, the relationships between them, and the key design patterns employed to ensure a scalable and maintainable architecture.

3.1 Class Definitions

The system is defined by a set of core classes and data classes.

Data Classes

Message

This is an immutable data class representing a single message.

Message

It encapsulates the message id, sender (User), content, and timestamp. Once a Message object is created, its state cannot be changed, which is ideal for representing historical records like chat messages.

Core Classes

User

Represents a participant in the chat system.

User

Each User has a unique id and a name. Crucially, it contains the onMessageReceived method, which acts as a callback for the Observer pattern, allowing a user to be "notified" of new messages.

Chat (Abstract Class): This class serves as the blueprint for all types of conversations. It manages a collection of members (User objects) and messages (Message objects). It defines the common behavior for all chats but delegates the specific implementation of retrieving the chat's name to its subclasses via the abstract getName method.

OneToOneChat

A concrete implementation of Chat designed for a private conversation between exactly two users.

GroupChat

A concrete implementation of Chat for conversations involving multiple users. It includes functionality to add or remove members.

ChatService

The central hub of the application.

Chat Service

It manages the lifecycle and registration of all users and chats. It acts as a go-between for all interactions, such as sending messages and creating new chats.

3.2 Class Relationships

The relationships between classes define the structure and interaction flow of the application.

Inheritance (Generalization)

  • OneToOneChat and GroupChat both extend the abstract Chat class. This is an "is-a" relationship, where both concrete classes are specialized types of a Chat. They inherit the common state (members, messages) and behavior (addMessage) while providing their own specific implementation for getName.

Composition

  • A Chat is composed of Messages. A Message cannot exist without being part of a Chat. The Chat class manages the lifecycle of the Message objects within its messages list. This strong "part-of" relationship is a classic example of composition. Chat (Whole) *---Message (Part)

Aggregation

  • A Chat aggregates Users as its members. A User can exist independently of any single chat and can be a member of multiple chats simultaneously. This is a "has-a" relationship, but the lifecycle of a User is not tied to the lifecycle of a Chat. Chat (Whole) <>---User (Part)
  • The ChatService aggregates all User and Chat objects in the system. It holds references to them in its maps but doesn't exclusively own them in a compositional sense.

Association

  • There is a unidirectional association from Message to User. Each Message object holds a reference to the User who sent it. This relationship is essential for identifying the sender of any given message. Message ---> User (sender)

3.3 Key Design Patterns

Several design patterns are utilized to create a decoupled and organized system.

Mediator Pattern

The ChatService is a textbook implementation of the Mediator pattern. It acts as a central communications hub, preventing User objects from needing to reference each other directly. All communication, like sending a message, is routed through the ChatService. This decouples users and chats, simplifying the system and making it easier to manage and extend. For example, a User sending a message only needs to know the chatId, not the details of all other recipients.

Observer Pattern

This pattern is used to notify users of new messages.

  • Subject: The ChatService acts as the subject. When its sendMessage method is called, it triggers an event (a new message).
  • Observer: The User class is the observer. Its onMessageReceived method is the callback that is invoked by the ChatService for every member of a chat when a new message is posted. This creates a push-based notification system.

Strategy Pattern

The abstract getName(User perspectiveUser) method in the Chat class and its concrete implementations in OneToOneChat and GroupChat exemplify the Strategy pattern. The algorithm (or strategy) for determining a chat's display name varies depending on the chat type.

  • OneToOneChat Strategy: The name is the name of the other user.
  • GroupChat Strategy: The name is the fixed groupName. This allows the client code to get the appropriate name polymorphically without needing to know the concrete type of the chat.

Factory Method (Simplified)

The ChatService class acts as a factory for creating core domain objects. Methods like createUser, createOneToOneChat, and createGroupChat centralize the instantiation logic. This simplifies the creation process for the client and ensures that all created objects are properly registered within the service.

3.4 Full Class Diagram

Simple Chat Application Class Diagram

4. Implementation

4.1 User

Represents a user in the chat system.

1class User:
2    def __init__(self, name: str):
3        self.id = str(uuid.uuid4())
4        self.name = name
5
6    def get_id(self) -> str:
7        return self.id
8
9    def get_name(self) -> str:
10        return self.name
11
12    def on_message_received(self, message: 'Message', chat_context: 'Chat'):
13        print(f"[Notification for {self.get_name()} in chat '{chat_context.get_name(self)}'] {message.get_sender().get_name()}: {message.get_content()}")
14
15    def __eq__(self, other):
16        if self is other:
17            return True
18        if other is None or type(self) != type(other):
19            return False
20        return self.id == other.id
21
22    def __hash__(self):
23        return hash(self.id)
24
25    def __str__(self):
26        return f"User{{id='{self.id}', name='{self.name}'}}"
  • Each user has a unique ID and name.
  • The onMessageReceived() method is called when the user receives a new message in a chat, simulating push notifications.

4.2 Message

Represents a single message sent in a chat.

1class Message:
2    def __init__(self, sender: User, content: str):
3        self.id = str(uuid.uuid4())
4        self.sender = sender
5        self.content = content
6        self.timestamp = datetime.now()
7
8    def get_id(self) -> str:
9        return self.id
10
11    def get_sender(self) -> User:
12        return self.sender
13
14    def get_content(self) -> str:
15        return self.content
16
17    def get_timestamp(self) -> datetime:
18        return self.timestamp
19
20    def __str__(self):
21        return f"[{self.timestamp}] {self.sender.get_name()}: {self.content}"

4.3 Chat (Abstract Class)

Abstract base class for all types of chats. Maintains a list of members and messages.

1class Chat(ABC):
2    def __init__(self):
3        self.id = str(uuid.uuid4())
4        self.members: List[User] = []
5        self.messages: List[Message] = []
6        self._lock = Lock()
7
8    def get_id(self) -> str:
9        return self.id
10
11    def get_members(self) -> List[User]:
12        with self._lock:
13            return copy.copy(self.members)
14
15    def get_messages(self) -> List[Message]:
16        with self._lock:
17            return copy.copy(self.messages)
18
19    def add_message(self, message: Message):
20        with self._lock:
21            self.messages.append(message)
22
23    @abstractmethod
24    def get_name(self, perspective_user: User) -> str:
25        pass

4.4 OneToOneChat

Concrete chat class for private messaging between two users.

1class OneToOneChat(Chat):
2    def __init__(self, user1: User, user2: User):
3        super().__init__()
4        self.members.extend([user1, user2])
5
6    def get_name(self, perspective_user: User) -> str:
7        for member in self.members:
8            if member != perspective_user:
9                return member.get_name()
10        return "Unknown Chat"

4.5 GroupChat

Represents a group chat with multiple members.

1class GroupChat(Chat):
2    def __init__(self, group_name: str, initial_members: List[User]):
3        super().__init__()
4        self.group_name = group_name
5        self.members.extend(initial_members)
6
7    def add_member(self, user: User):
8        with self._lock:
9            if user not in self.members:
10                self.members.append(user)
11
12    def remove_member(self, user: User):
13        with self._lock:
14            if user in self.members:
15                self.members.remove(user)
16
17    def get_name(self, perspective_user: User) -> str:
18        return self.group_name

4.6 ChatService (Mediator)

This service class is the heart of the application. It acts as a central Mediator that handles all user and chat management, as well as message routing.

1class ChatService:
2    def __init__(self):
3        self.users: Dict[str, User] = {}
4        self.chats: Dict[str, Chat] = {}
5        self._lock = Lock()
6
7    def create_user(self, name: str) -> User:
8        user = User(name)
9        with self._lock:
10            self.users[user.get_id()] = user
11        return user
12
13    def create_one_to_one_chat(self, user_id1: str, user_id2: str) -> Chat:
14        user1 = self.users.get(user_id1)
15        user2 = self.users.get(user_id2)
16        chat = OneToOneChat(user1, user2)
17        with self._lock:
18            self.chats[chat.get_id()] = chat
19        return chat
20
21    def create_group_chat(self, name: str, member_ids: List[str]) -> Chat:
22        members = []
23        for member_id in member_ids:
24            members.append(self.users.get(member_id))
25        chat = GroupChat(name, members)
26        with self._lock:
27            self.chats[chat.get_id()] = chat
28        return chat
29
30    def send_message(self, sender_id: str, chat_id: str, message_content: str):
31        sender = self.users.get(sender_id)
32        chat = self.chats.get(chat_id)
33        
34        if chat is None:
35            print(f"Error: Chat not found with ID: {chat_id}")
36            return
37
38        if sender not in chat.get_members():
39            print(f"Error: Sender {sender.get_name()} is not a member of this chat.")
40            return
41
42        message = Message(sender, message_content)
43        chat.add_message(message)
44
45        # Notify all members of the chat (Observer pattern)
46        for member in chat.get_members():
47            # Do not send a notification to the sender
48            if member != sender:
49                member.on_message_received(message, chat)
50
51    def print_chat_history(self, chat_id: str) -> List[Message]:
52        chat = self.chats.get(chat_id)
53        if chat is not None:
54            return chat.get_messages()
55        return []
56
57    def get_user_chats(self, user_id: str) -> List[Chat]:
58        user = self.users.get(user_id)
59        if user is None:
60            return []
61        
62        result = []
63        for chat in self.chats.values():
64            if user in chat.get_members():
65                result.append(chat)
66        return result

ChatService is a classic example of the Mediator pattern. Users do not communicate directly with each other; they communicate only through the ChatService. This decouples users and centralizes the complex communication logic, making the system easier to manage and extend.

sendMessage()

This method orchestrates the entire process of sending a message:

  1. It validates the sender and the chat.
  2. It creates the Message object.
  3. It adds the message to the chat's history.
  4. Notification (Observer): It iterates through all members of the chat and calls their onMessageReceived method, effectively pushing a notification to each recipient.

4.7 ChatApplicationDemo

This driver class demonstrates the end-to-end functionality of the system, acting as a client to the ChatService.

1class ChatApplicationDemo:
2    @staticmethod
3    def main():
4        # 1. Initialize the Mediator (ChatService)
5        chat_service = ChatService()
6
7        # 2. Create and register users
8        alice = chat_service.create_user("Alice")
9        bob = chat_service.create_user("Bob")
10        charlie = chat_service.create_user("Charlie")
11
12        print("--- Users registered in the system ---")
13        print()
14
15        # 3. Scenario 1: One-on-one chat between Alice and Bob
16        print("--- Starting one-on-one chat between Alice and Bob ---")
17        alice_bob_chat = chat_service.create_one_to_one_chat(alice.get_id(), bob.get_id())
18
19        # Alice sends a message to Bob
20        print("Alice sends a message...")
21        chat_service.send_message(alice.get_id(), alice_bob_chat.get_id(), "Hi Bob, how are you?")
22
23        # Bob sends a reply
24        print("\nBob sends a reply...")
25        chat_service.send_message(bob.get_id(), alice_bob_chat.get_id(), "I'm good, Alice! Thanks for asking.")
26        print()
27
28        # 4. Scenario 2: Group chat
29        print("--- Starting a group chat for a 'Project Team' ---")
30        project_members = [alice.get_id(), bob.get_id(), charlie.get_id()]
31        project_group = chat_service.create_group_chat("Project Team", project_members)
32
33        # Charlie sends a message to the group
34        print("Charlie sends a message to the group...")
35        chat_service.send_message(charlie.get_id(), project_group.get_id(), "Hey team, when is our deadline?")
36
37        # Alice replies to the group
38        print("\nAlice replies to the group...")
39        chat_service.send_message(alice.get_id(), project_group.get_id(), "It's next Friday. Let's sync up tomorrow.")
40        print()
41
42        # 5. Demonstrate fetching chat history
43        print("--- Fetching Chat Histories ---")
44
45        # History of Alice and Bob's chat
46        print(f"\nHistory for chat '{alice_bob_chat.get_name(alice)}':")
47        one_to_one_history = chat_service.print_chat_history(alice_bob_chat.get_id())
48        for message in one_to_one_history:
49            print(message)
50
51        # History of the project group chat
52        print(f"\nHistory for chat '{project_group.get_name(charlie)}':")
53        group_history = chat_service.print_chat_history(project_group.get_id())
54        for message in group_history:
55            print(message)
56
57        # 6. Demonstrate finding all of a user's chats
58        print("\n--- Fetching all of Alice's chats ---")
59        alice_chats = chat_service.get_user_chats(alice.get_id())
60        for chat in alice_chats:
61            print(f"Chat: {chat.get_name(alice)} (ID: {chat.get_id()})")
62
63
64if __name__ == "__main__":
65    ChatApplicationDemo.main()

5. Run and Test

Languages
Java
C#
Python
C++
Files7
core
entities
chat_application_demo.py
main
chat_service.py
chat_application_demo.py
Output

6. Quiz

Design Chat Application Quiz

1 / 21
Multiple Choice

Which core entity is responsible for creating new chats and managing chat-level operations in a chat application?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script